/**
 * Copyright 2018 orivil.com. All rights reserved.
 * Use of this source code is governed by a MIT-style
 * license that can be found at https://mit-license.org.
 */

package quce_la

import (
	"bytes"
	"encoding/json"
	"fmt"
	"gitee.com/tugoer/orivil/log"
	"gitee.com/tugoer/psy-testing/code/sites"
	"io"
	"net/url"
	"os"
	"sort"
	"strconv"
	"strings"
)

var site = "m.quce001.com"

// 更新, handle 用于更新内存中的数据
func DownloadLatest(lastSubjectID int, dir string, save func(sub *Subject, res map[string]*Result)) {
	var (
		subjectLogoDir = dir + "/subject/logo"
		subjectImageDir = dir + "/subject/img"
		questionDir = dir + "/subject/question"
		answerDir = dir + "/subject/answer"
		resultDir = dir + "/subject/result"
	)
	err := os.MkdirAll(subjectLogoDir, 0777)
	if err != nil {
		panic(err)
	}
	os.MkdirAll(subjectImageDir, 0777)
	os.MkdirAll(questionDir, 0777)
	os.MkdirAll(answerDir, 0777)
	os.MkdirAll(resultDir, 0777)
	var allList Lists
	page := 1
	stopDown:
		for {
			list, err := DownloadList(page)
			if err != nil {
				panic(err)
			}
			if len(list) > 0 {
				if lastSubjectID > 0 {
					/**
					Example:
						list: page 2 [6210, 6211, 6213, 6214, 6215] page 1 [6216, 6217, 6218, 6219, 6220]
						lastSubjectID: 6213
						append: [6214, 6215, 6216, 6217, 6218, 6219, 6220]
					 */
					for idx, li := range list {
						getID, err := strconv.Atoi(li.ID)
						if err != nil {
							panic(err)
						}
						if getID == lastSubjectID {
							if idx + 1 < len(list) {
								allList = append(list[idx + 1:], allList...)
							}
							break stopDown
						}
					}
				}
				page ++
				allList = append(list, allList...)
			} else {
				// 已到达最后一页
				break
			}
		}

	for _, li := range allList {
		sub, err := DownloadSubject(li.ID, subjectLogoDir, subjectImageDir, questionDir, answerDir)
		if err != nil {
			log.Warning.Printf("download subject %s got error:%s\n", li.ID, err)
		} else {
			if isSubjectOk(sub) {
				results, err := DownloadResults(sub, resultDir)
				if err != nil {
					log.Warning.Printf("download result %s got error:%s\n", li.ID, err)
				} else {
					sub.Top = li.Top
					save(sub, ResponseToResult(results))
				}
			} else {
				fmt.Printf("subject %s is not ok\n", li.ID)
			}
		}
	}
}

type List struct {
	Desc string `json:"desc"`
	ID string `json:"id"`
	Img string `json:"img"`
	IsGame string `json:"is_game"`
	Original string `json:"original"`
	Title string `json:"title"`
	View float64 `json:"view"`
	Top string `json:"top"`
	Utime string `json:"utime"`
}

type Lists []*List

func (ls Lists) Len() int {
	return len(ls)
}

func (ls Lists) Less(i, j int) bool {
	iID, err := strconv.Atoi(ls[i].ID)
	if err != nil {
		panic(err)
	}
	jID, err := strconv.Atoi(ls[j].ID)
	if err != nil {
		panic(err)
	}
	return iID < jID
}

func (ls Lists) Swap(i, j int) {
	ls[i], ls[j] = ls[j], ls[i]
}

func DownloadList(page int) (ls Lists, err error) {
	// 以 ID 排序
	ul := "http://"+site+"/index.php/wetest/index/getpagedata/order/id/pageIndex/" + strconv.Itoa(page)
	log.Info.Printf("downloading list: %s\n", ul)
	if res, err := sites.Get(ul, sites.Cookies, nil); err != nil {
		return nil, err
	} else {
		if res.StatusCode == 200 {
			var ls Lists
			err = json.NewDecoder(res.Body).Decode(&ls)
			res.Body.Close()
			if err != nil {
				return nil, err
			}
			if len(ls) > 0 {
				// 按 id 从小到大排序
				sort.Sort(ls)
				//for _, l := range ls {
				//	if l.IsGame == "0" || l.IsGame == "" {
				//		// 下载图片
				//		if l.Img != "" {
				//			iu := "http://uploads-admin.cdn.woquhudong.cn/" + l.Img
				//			file, err := sites.GetFile(iu, sites.Cookies, imageDir)
				//			if err != nil {
				//				return nil, err
				//			} else {
				//				// 将网络图片信息替换为本地图片地址
				//				l.Img = file
				//			}
				//		}
				//	}
				//}
			}
			return ls, nil
		} else {
			return nil, fmt.Errorf("url (%s) got response status: %d", ul, res.StatusCode)
		}
	}
}

type Result struct {
	Content *Content `json:"content"`
	Status int `json:"status"`
	Total []*Content `json:"total"`
	Attention *Attention `json:"attention"`
	MinWeight int `json:"min_weight"`
	MaxWeight int `json:"max_weight"`
}

type Response struct {
	Result
}

type Results map[string]*Result

func ResponseToResult(response map[string]*Response) (map[string]*Result) {
	results := make(map[string]*Result, len(response))
	for key, res := range response {
		results[key] = &res.Result
	}
	return results
}

func (res *Response) UnmarshalJSON(data []byte) error {
	mp := make(map[string]interface{}, 4)
	err := json.Unmarshal(data, &mp)
	if err != nil {
		return err
	}
	c := &Content{}
	err = json.Unmarshal([]byte(mp["content"].(string)), c)
	if err != nil {
		return err
	}
	res.Content = c
	var total []*Content
	err = json.Unmarshal([]byte(mp["total"].(string)), &total)
	if err != nil {
		return err
	}
	res.Total = total
	a := &Attention{}
	err = json.Unmarshal([]byte(mp["attention"].(string)), a)
	if err != nil {
		return err
	}
	res.Attention = a
	res.Status = int(mp["status"].(float64))
	return nil
}

type Content struct {
	Title string `json:"title"`
	Img string `json:"img"`
	Desc string `json:"desc"`
	ShareTitle string `json:"sharetitle"`
	OldImg string `json:"oldimg"`
	Threshold string `json:"threshold"`
}

type Attention struct {
	QRCode string `json:"qrcode"`
	Account int `json:"account"`
	Source int `json:"source"`
}

type Responses  map[string]*Response

func DownloadResults(subject *Subject, resultDir string) (results Responses, err error) {
	var options = make(map[string]struct{}, 5)
	switch subject.Type {
	// "1" 类主题只有一个问题，根据问题选项获得结果
	case "1":
		for _, question := range subject.Questions {
			for key := range question.Answer {
				options[strings.ToUpper(key)] = struct{}{}
			}
		}
	// "2" 类主题根据答案 weight 计算结果, 正常 weight 结果应当是一个区间,
	// 而抓取数据的时候只能以排列组合的方式获取每一种结果
	case "2":
		var weights [][]int
		for _, q := range subject.Questions {
			var ws []int
			for _, a := range q.Answer {
				ws = append(ws, weightToInt(a.Weight))
			}
			weights = append(weights, ws)
		}
		list := getAnswerList(weights)
		for _, l := range list {
			options[strconv.Itoa(l)] = struct{}{}
		}
	// "3" 类主题根据答案 next 计算结果
	case "3":
		for _, q := range subject.Questions {
			for _, a := range q.Answer {
				next := []rune(a.Next)[0]
				if 'A' <= next && next <= 'Z' {
					options[string([]rune{next})] = struct{}{}
				}
			}
		}
	}
	results = make(map[string]*Response, len(options))
	for option := range options {
		result, err := DownloadResult(subject.ID, option, resultDir)
		if err != nil {
			return nil, fmt.Errorf("get subject option [%s.%s] result got error: %s", subject.ID, option, err)
		} else {
			if subject.Type == "2" {
				weight, err := strconv.Atoi(option) // 1 2 3 4...
				if err != nil {
					return nil, fmt.Errorf("transform weight to number got error: %s", err)
				}
				option = result.Content.Threshold // A B C D
				if _, ok := results[option]; !ok {
					result.MinWeight = weight
					result.MaxWeight = weight
					results[option] = result
				} else {
					existRes := results[option]
					if weight < existRes.MinWeight {
						existRes.MinWeight = weight
					}
					if weight > existRes.MaxWeight {
						existRes.MaxWeight = weight
					}
				}
			} else {
				/**
			当主题 type 为 2 时，按 weight 获得结果，且 weight 是一个区间
				{
					"5432": {
						"A": {"MinWeight": 1, "MaxWeight": 3}
						"B": {"MinWeight": 4, "MaxWeight": 7}
						"C": {"MinWeight": 8, "MaxWeight": 11}
						"D": {"MinWeight": 12, "MaxWeight": 16}
						...
					}
				}
			 */
				results[option] = result
			}
		}
	}
	return results, nil
}

func DownloadResult(id, option, imgDir string) (result *Response, e error) {
	u := "http://"+site+"/index.php/wetest/entry/getresult"
	log.Info.Printf("downloading result option [%s.%s]\n", id, option)
	res, err := sites.Post(u, sites.Cookies, url.Values{"id": {id}, "option": {option}})
	if err != nil {
		return nil, err
	}
	buf := bytes.NewBuffer(nil)
	_, err = io.Copy(buf, res.Body)
	if err != nil {
		return nil, err
	}
	res.Body.Close()
	result = &Response{}
	err = json.Unmarshal(buf.Bytes(), result)
	if err != nil {
		return nil, err
	} else {
		if c := result.Content; c != nil {
			var iu string
			if c.Img != "" {
				iu = "http://uploads-admin.cdn.woquhudong.cn/" + c.Img
			} else if c.OldImg != "" {
				iu = "http://uploads-admin.cdn.woquhudong.cn/" + c.OldImg
			}
			if iu != "" {
				file, err := sites.GetFile(iu, sites.Cookies, imgDir)
				if err != nil {
					return nil, err
				} else {
					c.Img = file
				}
			}
		}
	}
	return result, nil
}

type Subject struct {
	ID string `json:"id"`
	Title string `json:"title"`
	Logo string `json:"logo"`
	Desc string `json:"desc"`
	Questions []*Question `json:"question"`
	Original string `json:"original"`
	Type string `json:"type"`
	Cid string `json:"cid"`
	Img string `json:"img"`
	TplID string `json:"tpl_id"`
	IsGame string `json:"is_game"`
	ShareUrl string `json:"share_url"`
	View string `json:"view"`
	Top string `json:"top"`
	LikeNum string `json:"like_num"`
	Utime string `json:"utime"`
	Subscribe string `json:"subscribe"`
	NewPlan string `json:"new_plan"`
	Author string `json:"author"`
	Status string `json:"status"`
	SpecialResult string `json:"special_result"`
}

type FakeSubject struct {
	Questions string `json:"question"`
	*Subject
}

type Question struct {
	Img string `json:"img"`
	Question string `json:"question"`
	Audio string `json:"audio"`
	Weight interface{} `json:"weight"`
	Merge bool `json:"merge"`
	Answer map[string]*Answer `json:"answer"`
}

type Answer struct {
	Title string `json:"title"`
	Weight interface{} `json:"weight"`
	Next string `json:"next"`
	Img string `json:"img"`
}

func DownloadSubject(id, subLogoDir, subImgDir, questionDir, answerDir string) (sub *Subject, err error) {
	u := "http://"+site+"/index.php/wetest/entry/index/id/" + id
	log.Info.Printf("downloading subject: %s\n", u)
	if res, err := sites.Get(u, sites.Cookies, nil); err != nil {
		return nil, err
	} else {
		buf := bytes.NewBuffer(nil)
		_, err := io.Copy(buf, res.Body)
		res.Body.Close()
		if err != nil {
			return nil, err
		}
		data := buf.Bytes()
		prefix := []byte("var gameData =")
		idx := bytes.Index(data, prefix)
		if idx < 0 {
			return nil, nil
		}
		data = data[idx + len(prefix):]
		suffix := []byte(";")
		data = data[:bytes.Index(data, suffix)]
		fakeSub := &FakeSubject{}
		err = json.Unmarshal(data, fakeSub)
		if err != nil {
			fmt.Printf("unmarshal data: %s\n", string(data))
			return nil, err
		}
		sub = fakeSub.Subject
		err = json.Unmarshal([]byte(fakeSub.Questions), &sub.Questions)
		if err != nil {
			return nil, err
		}
		if sub.Logo != "" {
			ui := "http://uploads-admin.cdn.woquhudong.cn/" + sub.Logo
			logo, err := sites.GetFile(ui, sites.Cookies, subLogoDir)
			if err != nil {
				return nil, err
			}
			sub.Logo = logo
		}
		if sub.Img != "" {
			ui := "http://uploads-admin.cdn.woquhudong.cn/" + sub.Img
			img, err := sites.GetFile(ui, sites.Cookies, subImgDir)
			if err != nil {
				return nil, err
			}
			sub.Img = img
		}
		for _, q := range sub.Questions {
			if q.Img != "" {
				ui := "http://uploads-admin.cdn.woquhudong.cn/" + q.Img
				img, err := sites.GetFile(ui, sites.Cookies, questionDir)
				if err != nil {
					return nil, err
				}
				q.Img = img
			}
			for _, a := range q.Answer {
				if a.Img != "" {
					ui := "http://uploads-admin.cdn.woquhudong.cn/" + a.Img
					img, err := sites.GetFile(ui, sites.Cookies, answerDir)
					if err != nil {
						return nil, err
					}
					a.Img = img
				}
			}
		}
		return sub, err
	}
}

func isSubjectOk(sub *Subject) bool {
	if sub == nil {
		return false
	}
	// 检查当主题 type 为 "2" 时(即以 weight 计算), 有可能所有答案的 weight 都为 0, 此时将出现随机结果
	// 此结果无法抓取, 因此需要将这类题目过滤
	switch sub.Type {
	case "2":
		totalWeight := 0
		for _, q := range sub.Questions {
			for _, a := range q.Answer {
				totalWeight += weightToInt(a.Weight)
			}
		}
		if totalWeight == 0 {
			return false
		}
	// next 有可能为 "", 如 http://w.xingzuozhuanjia.com/index.php/wetest/entry/index/id/117
	// answer key 有可能为 "undefined", 如 http://w.xingzuozhuanjia.com/index.php/wetest/entry/index/id/113
	case "3":
		for _, q := range sub.Questions {
			for key, a := range q.Answer {
				if len(a.Next) == 0 || key == "undefined" {
					return false
				}
			}
		}
	}
	return true
}